不知道讀者有沒有想過,如果今天我想要把每一步 monadic 操作的過程都有 log 紀錄方便我們 debug 的話要怎麼做?(用 print 大法也不是不行)
Writer 可以協助我們達成這方面的事情,我們可以利用它將每次計算的額外結果記錄起來並帶到下一次計算。
newtype Writer w a = Writer { runWriter :: (a, w) }
instance (Monoid w) => Monad (Writer w) where
return x = Writer (x, mempty)
(Writer (x,v)) >>= f = let (Writer (y, v')) = f x in Writer (y, v `mappend` v')
首先這邊利用 newtype 封裝了這個型別也就是 (a,w) 這種形式,且提供 runWriter 讓我們取值。
它對於 Monad 實作說明了, m (也就是通常拿來做 log 的部分)要是 Monoid ,至於為什麼要是 Monoid 呢?因為 monoid 的 identity 的特性所以我們可以將 return x (或者想像成預設的 context )變成 Writer (x, mempty) ,這樣子我們就能確保之後不管是什麼值用 mappend 進行運算,結果都會是我們傳進來的值。
而 >>= 則是利用 pattern matching 將 f x 計算的結果把 y , v' 取出來並將 y 以及 v' mappend v' ,塞回去 context 。
我們先來寫一個小 function 來表示一個 Int 被裝進去 Writer 後要有一個 log
logNumber :: Int -> Writer [String] Int
logNumber x = writer (x, ["Got number: " ++ show x])
writer是Writer所提供的 function ,當然也可以直接用return x來 wrap 後,再添加 log 來達成一樣的效果
然後寫計算這種 monadic value 的 function
addWithLog :: Writer [String] Int -> Writer [String] Int -> Writer [String] Int
addWithLog a b = do
x <- a
y <- b
tell ["added "++ show x ++ " and " ++ show y]
return (x + y)
這邊的程式碼就蠻簡單的,就單純的把 a 及 b 這兩個 monadic value 從 context 取出來後,利用 tell 添加 log ,然後再用 return 包裝一次 context,至於 tell 簡單來說就是一個添加 log 的 function 。
我們來實際跑跑看
print $ runWriter $ addWithLog (logNumber 10) (logNumber 1)
-- (11,["Got number: 10","Got number: 1","added 10 and 1"])
print $ runWriter $ addWithLog (logNumber 1) $ addWithLog (logNumber 2) (logNumber 3)
-- (6,["Got number: 1","Got number: 2","Got number: 3","added 2 and 3","added 1 and 5"])
結果如我們預期的那樣,我們每次獲得數字以及運算的結果都有寫進去 Writer 裡。
今天的程式碼
https://github.com/toddLiao469469/30days-for-haskell